package IC.SemanticCheckes;

import java.util.*;

import IC.AST.*;
import IC.SymbolTable.*;
import IC.Types.*;
import IC.Types.Type;


//check for:
// 1. redefinitions/hiding/overriding  of fields, parameters and methods
// 2/ rules of main
public class SymbolTableBuilder implements IC.AST.Visitor{


	public SymbolTableBuilder(String icFilePath){
		TypeTable.initTypeTable(icFileName);
	}
	
	public String getFileName(){
		return this.icFileName;
	}
	
	private static boolean isMainMethod(MethodSymbol ms, Method m){
		if (!ms.isStatic()) {
			return false;
		} 
		// check if method name is "main"
		if (ms.getName().compareTo("main") != 0) {
			return false;
		}
		IC.Types.MethodType mt = (IC.Types.MethodType) ms.getType();
		// is return type is void
		if (!mt.getReturnType().subtypeOf(TypeTable.getType("void"))) {
			return false; 
		}
		Iterator<IC.Types.Type> paramTypesIter = mt.getParamTypes().iterator();
		// is got parameters
		if (!paramTypesIter.hasNext()) {
			return false; 
		}
		IC.Types.Type t = paramTypesIter.next();
		// are parameters String[]
		if (!t.subtypeOf(TypeTable.arrayType(TypeTable.getType("string")))) {
			return false;
		}
		if (paramTypesIter.hasNext()) {
			return false;
		}
		return true;
	}
	
	// 1. creates GlobalSymbolTable
	// 2. recursive call to class visitor
	// Assumption: The Program class is the only with null enclosingScope
	public Object visit(Program program){
		// create a new global symbol table, to be returned at the end of construction
		GlobalSymbolTable global = new GlobalSymbolTable(icFileName);

		// add classes to global and updates the type table
		for (ICClass c: program.getClasses()){
			try{
				global.addClass(c);
			} catch (SemanticError se){
				// class is previously defined or super class is not defined
				se.setLine(c.getLine());
				throw se;
			}
		}
		
		// recursive class symbol tables build
		for (ICClass c: program.getClasses()){
			// set enclosing scope
			c.setEnclosingScope(global);
			ClassSymbolTable cst = (ClassSymbolTable) c.accept(this);
			if(c.hasSuperClass()){
				global.getClassSymbolTable(c.getSuperClassName()).addClassSymbolTable(cst);
			} else {
				global.addClassSymbolTable(cst);
			}			
		}
		
		// check if has main method
		if (!hasMain){
			throw new SemanticError("Program has no main method",0,"");
		}
		return global;
	}
	
	
	// 1. creates ClassSymbolTable
	// 2. recursive call to fields and methods visitor
	public Object visit(ICClass icClass) {
		ClassSymbolTable cst;
		GlobalSymbolTable global = (GlobalSymbolTable) icClass.getEnclosingScope();
		
		// create class symbol table
		if (icClass.hasSuperClass()) {
			cst = new ClassSymbolTable(icClass.getName(),global);
		} else { // no superclass
			cst = new ClassSymbolTable(icClass.getName(),global);
		}
		
		// recursively build fields class symbol table
		for (Field f: icClass.getFields()){
			// set enclosing scope
			f.setEnclosingScope(cst);
			boolean isFieldDefined = false;
			boolean isMethodDefined = false;
			// check if previously defined
			try{
				cst.getFieldSymbol(f.getName());
				// if got till here, field is previously defined as field, print error
				isFieldDefined = true;
				throw new SemanticError("field is previously defined",f.getLine(),f.getName());
			} catch (SemanticError se){
				if (isFieldDefined){
					throw se;
				}
				try{
					cst.getMethodSymbol(f.getName());
					// if got till here, field is previously defined as method, print error
					isMethodDefined = true;
					throw new SemanticError("field is previously defined",f.getLine(),f.getName());
				} catch (SemanticError se2){ 
					if (isMethodDefined){
						throw se2;
					}
					// field is not previously defined
					try{
						cst.addFieldSymbol(f.getName(), f.getType().getBracketName());
					} catch (SemanticError se3){
						// the field's type is undefined
						se3.setLine(f.getLine());
						throw se3;
					}
				}
			}
			// recursively call visitor
			f.accept(this);
		}
		
		// recursively build methods class symbol table
		for (Method m: icClass.getMethods()){
			// set enclosing scope
			m.setEnclosingScope(cst);
			// create MethodSymbol
			MethodSymbol ms;
			try{
				List<Type> paramTypes = new ArrayList<Type>();
				for (Formal f: m.getFormals()){
					paramTypes.add(TypeTable.getType(f.getType().getBracketName())); 
				}	
				ms = new MethodSymbol(m.getName(), TypeTable.getType(m.getType().getBracketName()), paramTypes, m.isMethodStatic());
				if (isMainMethod(ms, m)){
					if (hasMain){
						// already have "main" method, throw error
						throw new SemanticError("Program already has main method", m.getLine(), ms.getName());
					} else {
						hasMain = true;
					}
				}
			} catch (SemanticError se){
				se.setLine(m.getLine());
				throw se;
			}
			
			boolean isMethodDefined;
			boolean isFieldDefined = false;
			MethodSymbol prevMS = null;

			try{
				cst.getFieldSymbol(m.getName());
				// if got here, method is previously defined as field, print error
				isFieldDefined = true;
				throw new SemanticError("method is previously defined",m.getLine(),m.getName());
			} catch (SemanticError e){ // e will not be handled
				if (isFieldDefined){
					throw e;
				}
				try{
					prevMS = cst.getMethodSymbol(m.getName());
					isMethodDefined = true;
				}catch (SemanticError e2){
					isMethodDefined = false;
				}
				// if got here, method is previously defined in this class, print error
				if ( isMethodDefined && !prevMS.isInherited)
				{
					throw new SemanticError("method is previously defined",m.getLine(),m.getName());
				}else{
					if (isMethodDefined){
						if (!prevMS.getType().equals(ms.getType()) || (prevMS.isStatic() != ms.isStatic())){
							// if got here, method is previously defined in super-class with a different signature, print error
							throw new SemanticError("method is previously defined, overloading not allowed",m.getLine(),m.getName());
						} else { 
							// overriding method, the new method will be with isInherited=false
							cst.addMethodSymbol(m.getName(), ms);
						}
					}else{
						// method is not previously defined
						cst.addMethodSymbol(m.getName(), ms);							
					}
				}
			}
		}
		
		// recursive method symbol table build
		for (Method m: icClass.getMethods()){
			MethodSymbolTable mst = (MethodSymbolTable) m.accept(this);
			cst.addMethodSymbolTable(mst);
		}
		
		return cst;
	}

	// 1. creates MethodSymbolTable
	// 2. recursive call to parameters and statements visitor
	public MethodSymbolTable methodVisit(Method method){
		// create method symbol table
		MethodSymbolTable mst = new MethodSymbolTable(method.getName(),(ClassSymbolTable)method.getEnclosingScope());
		
		// add return type symbol
		// set enclosing scope for return type
		method.getType().setEnclosingScope(mst);
		try{
			mst.addVarSymbol("_ret", "RET_VAR", method.getType().getBracketName(),0);
		} catch (SemanticError se){
			// semantic error while creating the return type symbol (some semantic type error)
			se.setLine(method.getLine());
			throw se;
		}
		
		// fill method symbol table with parameters (formals)
		for (Formal f: method.getFormals()){
			// set enclosing scope
			f.setEnclosingScope(mst);
			boolean isParamDefined = false;
			try{
				mst.getVarParamSymbol(f.getName());
				isParamDefined = true;
				// if got here, parameter is previously defined in this method
				throw new SemanticError("parameter is previously defined in method "+method.getName(),f.getLine(),f.getName());
			} catch (SemanticError e){ // e will not be handled
				if (isParamDefined){
					throw e;
				}
				// parameter is undefined, insert its symbol to this method symbol table
				try{
					mst.addVarSymbol(f.getName(), "PARAM" , f.getType().getBracketName(),0);
				} catch (SemanticError se){
					// semantic error while creating the parameter type symbol (some semantic type error)
					se.setLine(f.getLine());
					throw se;
				}
			}
			// recursive call to visitor
			f.accept(this);
		}
		
		// recursive call to visitor
		for (Statement s: method.getStatements()){
			// set enclosing scope
			s.setEnclosingScope(mst);
			s.accept(this); 
		}

		return mst;
	}
	
	public Object visit(VirtualCall call) {
		if (call.isExternal()) { // field location is not null
			call.getLocation().setEnclosingScope(call.getEnclosingScope());
			call.getLocation().accept(this);
		}		
		for (Expression e: call.getArguments()){
			e.setEnclosingScope(call.getEnclosingScope());
			e.accept(this);
		}		
		return true;
	}
	
	public Object visit(StaticCall call) {
		for (Expression e: call.getArguments()){
			e.setEnclosingScope(call.getEnclosingScope());
			e.accept(this);
		}
		
		return true;
	}
	
	public Object visit(StaticMethod method) {
		MethodSymbolTable mst = methodVisit(method);
		return mst;
	}
	
	public Object visit(VirtualMethod method) {
		MethodSymbolTable mst = methodVisit(method);
		return mst;
	}

	public Object visit(LibraryMethod method) {
		MethodSymbolTable mst = methodVisit(method);
		return mst;
	}
	

	// 1. creates BlockSymbolTable
	// 2. recursive call to statements
	public Object visit(StatementsBlock statementsBlock) {
		BlockSymbolTable bst = new BlockSymbolTable(statementsBlock.getEnclosingScope());
		// get this bst's father (block / method symbol table)
		BlockSymbolTable bst_father = (BlockSymbolTable) statementsBlock.getEnclosingScope();
		
		// recursive call to visitor
		for (Statement s: statementsBlock.getStatements()){
			// set enclosing scope
			s.setEnclosingScope(bst);
			s.accept(this);
		}
		
		// add this block symbol table to its father's (method/block symbol table) bst list
		bst_father.addBlockSymbolTable(bst);
		return bst;
	}
	

	public Object visit(LocalVariable localVariable) {
		BlockSymbolTable bst = (BlockSymbolTable)localVariable.getEnclosingScope();
		boolean isVarDefined;
		VarSymbol vs = null;
		try{
			vs = bst.getVarSymbol(localVariable.getName(),0);
			isVarDefined = true;
		} catch (SemanticError e){
			isVarDefined = false;
		}
		// if got here, local variable is previously defined in this method / block
		if (isVarDefined && !vs.isDefined)
		{
			throw new SemanticError("variable is previously defined", localVariable.getLine(), localVariable.getName());
		}	
		// local variable is undefined, insert its symbol to block/method symbol table
		try{
			bst.addVarSymbol(localVariable.getName(), "VAR" ,localVariable.getType().getBracketName(),localVariable.getLine());
		} catch (SemanticError se){
			// semantic error while creating the local variable type symbol (some semantic type error)
			se.setLine(localVariable.getLine());
			throw se;
		}
		 
		// recursive call to visitor
		if (localVariable.hasInitValue()){
			localVariable.getInitValue().setEnclosingScope(localVariable.getEnclosingScope());
			localVariable.getInitValue().accept(this);
		}
		
		localVariable.getType().setEnclosingScope(localVariable.getEnclosingScope());
		localVariable.getType().accept(this);
		
		return true;
	}

	
	public Object visit(Assignment assignment) {
		// recursive call to visitor
		assignment.getVariable().setEnclosingScope(assignment.getEnclosingScope());
		assignment.getVariable().accept(this);
		
		assignment.getAssignment().setEnclosingScope(assignment.getEnclosingScope());
		assignment.getAssignment().accept(this);
		
		return true;
	}
	
	
	public Object visit(CallStatement callStatement) {
		callStatement.getCall().setEnclosingScope(callStatement.getEnclosingScope());
		callStatement.getCall().accept(this);
		
		return true;
	}
	
	public Object visit(Break breakStatement) {
		return true;
	}

	public Object visit(If ifStatement) {
		ifStatement.getCondition().setEnclosingScope(ifStatement.getEnclosingScope());
		ifStatement.getCondition().accept(this);
		
		// in case of a LocalVariable statement, create new BlockSymbolTable
		Statement operation = ifStatement.getOperation();
		if (operation instanceof LocalVariable){
			BlockSymbolTable bst = new BlockSymbolTable(ifStatement.getEnclosingScope());
			((BlockSymbolTable)ifStatement.getEnclosingScope()).addBlockSymbolTable(bst);
			operation.setEnclosingScope(bst);	
		} else {
			operation.setEnclosingScope(ifStatement.getEnclosingScope());
		}
		operation.accept(this);
		
		if (ifStatement.hasElse()){
			// in case of a LocalVariable statement, create new BlockSymbolTable
			Statement elseOperation = ifStatement.getElseOperation();
			if (elseOperation instanceof LocalVariable){
				BlockSymbolTable bst = new BlockSymbolTable(ifStatement.getEnclosingScope());
				((BlockSymbolTable)ifStatement.getEnclosingScope()).addBlockSymbolTable(bst);
				elseOperation.setEnclosingScope(bst);	
			} else {
				elseOperation.setEnclosingScope(ifStatement.getEnclosingScope());
			}
			elseOperation.accept(this);
		}
		
		return true;
	}

	public Object visit(Continue continueStatement) {
		return true;
	}


	public Object visit(Return returnStatement) {
		if (returnStatement.hasValue()){
			returnStatement.getValue().setEnclosingScope(returnStatement.getEnclosingScope());
			returnStatement.getValue().accept(this);
		}
		
		return true;
	}

	public Object visit(While whileStatement) {
		whileStatement.getCondition().setEnclosingScope(whileStatement.getEnclosingScope());
		whileStatement.getCondition().accept(this);
		
		// in case of a LocalVariable statement, create new BlockSymbolTable
		Statement operation = whileStatement.getOperation();
		if (operation instanceof LocalVariable){
			BlockSymbolTable bst = new BlockSymbolTable(whileStatement.getEnclosingScope());
			((BlockSymbolTable)whileStatement.getEnclosingScope()).addBlockSymbolTable(bst);
			operation.setEnclosingScope(bst);	
		} else {
			operation.setEnclosingScope(whileStatement.getEnclosingScope());
		}
		operation.accept(this);
		
		return true;
	}
	
	public Object visit(ArrayLocation location) {
		location.getArray().setEnclosingScope(location.getEnclosingScope());
		location.getArray().accept(this);
		
		location.getIndex().setEnclosingScope(location.getEnclosingScope());
		location.getIndex().accept(this);		
		return true;
	}
	

	public Object visit(ExpressionBlock expressionBlock) {
		expressionBlock.getExpression().setEnclosingScope(expressionBlock.getEnclosingScope());
		expressionBlock.getExpression().accept(this);		
		return true;
	}

	public Object visit(Formal formal) {
		formal.getType().setEnclosingScope(formal.getEnclosingScope());
		formal.getType().accept(this);	
		return true;
	}
	
	public Object visit(Field field) {
		field.getType().setEnclosingScope(field.getEnclosingScope());
		field.getType().accept(this);	
		return true;
	}


	public Object visit(VariableLocation location) {
		if (location.isExternal()){ // field location is not null
			location.getLocation().setEnclosingScope(location.getEnclosingScope());
			location.getLocation().accept(this);
		}else try{
			// check that the location is a previously defined variable
			((BlockSymbolTable) location.getEnclosingScope()).getVarSymbol(location.getName(),0);
		} catch (SemanticError se){
			se.setLine(location.getLine());
			throw se;
		}
		
		return true;
	}
	
	public Object visit(Length length) {
		length.getArray().setEnclosingScope(length.getEnclosingScope());
		length.getArray().accept(this);
		
		return true;
	}

	public Object visit(Literal literal) {
		return true;
	}

	public Object binaryOpVisit(BinaryOp binaryOp){
		binaryOp.getFirstOperand().setEnclosingScope(binaryOp.getEnclosingScope());
		binaryOp.getFirstOperand().accept(this);
		
		binaryOp.getSecondOperand().setEnclosingScope(binaryOp.getEnclosingScope());
		binaryOp.getSecondOperand().accept(this);
		
		return true;
	}
	

	public Object visit(LogicalBinaryOp binaryOp) {
		return binaryOpVisit(binaryOp);
	}
	

	public Object visit(MathBinaryOp binaryOp) {
		return binaryOpVisit(binaryOp);
	}
	

	public Object unaryOpVisit(UnaryOp unaryOp){
		unaryOp.getOperand().setEnclosingScope(unaryOp.getEnclosingScope());
		unaryOp.getOperand().accept(this);	
		return true;
	}


	public Object visit(LogicalUnaryOp unaryOp) {
		return unaryOpVisit(unaryOp);
	}

	public Object visit(MathUnaryOp unaryOp) {
		return unaryOpVisit(unaryOp);
	}

	public Object visit(NewArray newArray) {
		newArray.getType().setEnclosingScope(newArray.getEnclosingScope());
		newArray.getType().accept(this);
		
		newArray.getSize().setEnclosingScope(newArray.getEnclosingScope());
		newArray.getSize().accept(this);
		
		return true;
	}

	public Object visit(NewClass newClass) {
		return true;
	}

	public Object visit(PrimitiveType type) {
		return true;
	}

	public Object visit(This thisExpression) {
		return true;
	}

	public Object visit(UserType type) {
		return true;
	}



	@Override
	public Object visit(FieldOrMethodList field_or_method) {
		// TODO Auto-generated method stub
		return null;
	}

	private String icFileName;
	private boolean hasMain = false;
	
}
